MoneyBag.java

package money;

import java.util.ArrayList;
import java.util.List;


/**
 * A MoneyBag defers exchange rate conversions.
 *
 * For example adding 12 Swiss Francs to 14 US Dollars is represented
 * as a bag containing the two Monies 12 CHF and 14 USD. Adding another
 * 10 Swiss francs gives a bag with 22 CHF and 14 USD. Due to the
 * deferred exchange rate conversion we can later value a MoneyBag with
 * different exchange rates.
 *
 * @author Kent Beck
 * @author Robert Duvall (small updates and commenting)
 */
public class MoneyBag extends Money {
    private final List<SimpleMoney> fMonies;

    /**
     * Static "constructor" that creates appropriate concrete class based on the given parameters
     *
     * @param monies any number of simple monies to combine
     */
    public static Money of (SimpleMoney ... monies) {
        MoneyBag result = new MoneyBag();
        for (SimpleMoney m : monies) {
            result.appendTo(m);
        }
        return result.simplify();
    }

    /**
     * Constructs an empty bag of money.
     */
    public MoneyBag () {
        fMonies = new ArrayList<>(5);
    }

    /**
     * Constructs a bag of money from the contents of the given bag of money.
     */
    public MoneyBag (MoneyBag copy) {
        fMonies = new ArrayList<>(copy.fMonies);
    }


    /**
     * @see Money#add(Money)
     *
     * Forwards the request to addMoney helper, since we know what concrete type this instance is
     */
    @Override
    public Money add (Money m) {
        return m.addMoneyBag(this);
    }

    /**
     * @see Money#subtract(Money)
     */
    @Override
    public Money subtract (Money m) {
        return add(m.negate());
    }

    /**
     * @see Money#multiply(int)
     */
    @Override
    public Money multiply (int factor) {
        MoneyBag result = new MoneyBag();
        if (factor != 0) {
            for (SimpleMoney each : fMonies) {
                // multiply returns Money abstraction, but we know it is SimpleMoney
                result.appendTo((SimpleMoney)each.multiply(factor));
            }
        }
        return result;
    }

    /**
     * @see Money#negate()
     */
    @Override
    public Money negate () {
        MoneyBag result = new MoneyBag();
        for (SimpleMoney each : fMonies) {
            // negate returns Money abstraction, but we know it is SimpleMoney
            result.appendTo((SimpleMoney)each.negate());
        }
        return result;
    }

    /**
     * @see Money#isZero()
     */
    @Override
    public boolean isZero () {
        return fMonies.size() == 0;
    }

    /**
     * @see Object#equals(Object)
     */
    @Override
    public boolean equals (Object anObject) {
        if (isZero()) {
            return anObject instanceof Money && ((Money)anObject).isZero();
        }
        if (anObject instanceof MoneyBag aMoneyBag) {
            if (aMoneyBag.fMonies.size() != fMonies.size()) {
                return false;
            }
            for (SimpleMoney each : fMonies) {
                if (!aMoneyBag.contains(each)) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    /**
     * @see Object#hashCode()
     */
    @Override
    public int hashCode () {
        int hash = 0;
        for (SimpleMoney each : fMonies) {
            hash ^= each.hashCode();
        }
        return hash;
    }

    /**
     * @see Object#toString()
     */
    @Override
    public String toString () {
        StringBuilder sb = new StringBuilder();
        sb.append("{");
        for (SimpleMoney each : fMonies) {
            sb.append(each);
        }
        sb.append("}");
        return sb.toString();
    }

    // Double dispatch methods and their helpers --- one needed for each different subclass!
    @Override
    Money addMoney (SimpleMoney simple) {
        MoneyBag result = new MoneyBag(this);
        result.appendTo(simple);
        return result.simplify();
    }

    private void appendTo (SimpleMoney aSimpleMoney) {
        if (aSimpleMoney.isZero()) {
            return;
        }

        Money old = findMoney(aSimpleMoney.currency());
        if (old == null) {
            fMonies.add(aSimpleMoney);
            return;
        }
        fMonies.remove(old);
        SimpleMoney sum = (SimpleMoney)old.add(aSimpleMoney);
        if (sum.isZero()) {
            return;
        }
        fMonies.add(sum);
    }

    @Override
    Money addMoneyBag (MoneyBag bag) {
        MoneyBag result = new MoneyBag(this);
        result.appendTo(bag);
        return result.simplify();
    }

    private void appendTo (MoneyBag aBag) {
        for (SimpleMoney each : aBag.fMonies) {
            appendTo(each);
        }
    }

    // Find first money in this bag matching given currency
    private SimpleMoney findMoney (String currency) {
        for (SimpleMoney each : fMonies) {
            if (each.currency().equals(currency)) {
                return each;
            }
        }
        return null;
    }

    // Returns true only if given money is in this bag.
    private boolean contains (SimpleMoney m) {
        SimpleMoney found = findMoney(m.currency());
        return found != null && found.amount() == m.amount();
    }


    // Simplifies this money bag if it is not needed
    private Money simplify () {
        if (fMonies.size() == 1) {
            return fMonies.iterator().next();
        }
        return this;
    }
}